#!/usr/bin/env python3
# This alternative Reader.py script was meant to cover not only USB readers but more.
# It can be used to replace Reader.py if you have readers such as
# MFRC522, RDM6300 or PN532.
# Please use the github issue threads to share bugs and improvements
# or create pull requests.
import multiprocessing
try:
    from multiprocessing import SimpleQueue
except ImportError:
    from multiprocessing.queues import SimpleQueue
import os.path
import sys
import serial
import string
import RPi.GPIO as GPIO
import logging
from enum import Enum
from evdev import InputDevice, ecodes, list_devices

logger = logging.getLogger(__name__)


class EDevices(Enum):
    MFRC522 = 0
    RDM6300 = 1
    PN532 = 2


def get_devices():
    devices = [InputDevice(fn) for fn in list_devices()]
    devices.append(NonUsbDevice(EDevices.MFRC522.name))
    devices.append(NonUsbDevice(EDevices.RDM6300.name))
    devices.append(NonUsbDevice(EDevices.PN532.name))
    return devices


class NonUsbDevice(object):
    name = None

    def __init__(self, name, phys=''):
        self.name = name
        self.phys = phys


class UsbReader(object):
    def __init__(self, device):
        self.keys = "X^1234567890XXXXqwertzuiopXXXXasdfghjklXXXXXyxcvbnmXXXXXXXXXXXXXXXXXXXXXXX"
        self.dev = device

    def readCard(self):
        from select import select
        stri = ''
        key = ''
        while key != 'KEY_ENTER':
            select([self.dev], [], [])
            for event in self.dev.read():
                if event.type == 1 and event.value == 1:
                    stri += self.keys[event.code]
                    key = ecodes.KEY[event.code]
        return stri[:-1]


class Mfrc522Reader(object):
    def __init__(self):
        import pirc522
        self.device = pirc522.RFID()
        path = os.path.dirname(os.path.realpath(__file__))
        readmode_uid = False
        if os.path.isfile(path + '/../settings/Rfidreader_Rc522_Readmode_UID'):
            with open(path + '/../settings/Rfidreader_Rc522_Readmode_UID', 'r') as f:
                readmode_uid = f.read().rstrip().split(';', 1)[0] == 'ON'
        self._read_function = self._readCard_normal if readmode_uid else self._readCard_legacy

    def _readCard_legacy(self):
        # Scan for cards
        (error, tag_type) = self.device.request()

        if not error:
            logger.info("Card detected.")
            # Perform anti-collision detection to find card uid
            (error, uid) = self.device.anticoll()
            if not error:
                card_id = ''.join((str(x) for x in uid))
                logger.info(card_id)
                return card_id
        logger.debug("No Device ID found.")
        return None

    def _readCard_normal(self):
        # Scan for cards
        uid = self.device.read_id(as_number=True)
        if not uid:
            logger.debug("No Device ID found.")
            return None
        card_id = str(uid)
        logger.info("Card detected.")
        logger.info(card_id)
        return card_id

    def readCard(self):
        # Scan for cards
        self.device.wait_for_tag()
        return self._read_function()

    @staticmethod
    def cleanup():
        GPIO.cleanup()


class Rdm6300Reader:
    def __init__(self):
        device = '/dev/ttyS0'
        baudrate = 9600
        ser_timeout = 0.1
        self.last_card_id = ''
        try:
            self.rfid_serial = serial.Serial(device, baudrate, timeout=ser_timeout)
        except serial.SerialException as e:
            logger.error(e)
            exit(1)

    def readCard(self):
        byte_card_id = b''

        try:
            while True:
                try:
                    read_byte = self.rfid_serial.read()

                    if read_byte == b'\x02':    # start byte
                        while read_byte != b'\x03':     # end bye
                            read_byte = self.rfid_serial.read()
                            byte_card_id += read_byte

                        card_id = byte_card_id.decode('utf-8')
                        byte_card_id = ''
                        card_id = ''.join(x for x in card_id if x in string.printable)

                        # Only return UUIDs with correct length
                        if len(card_id) == 12 and card_id != self.last_card_id:
                            self.last_card_id = card_id
                            self.rfid_serial.reset_input_buffer()
                            return self.last_card_id

                        else:   # wrong UUID length or already send that UUID last time
                            self.rfid_serial.reset_input_buffer()

                except ValueError as ve:
                    logger.errror(ve)

        except serial.SerialException as se:
            logger.error(se)

    def cleanup(self):
        self.rfid_serial.close()


class Pn532Reader:
    def __init__(self):
        from py532lib.i2c import Pn532_i2c
        from py532lib.mifare import Mifare
        from py532lib.mifare import MIFARE_WAIT_FOR_ENTRY
        pn532 = Pn532_i2c() # noqa F841
        self.device = Mifare()
        self.device.SAMconfigure()
        self.device.set_max_retries(MIFARE_WAIT_FOR_ENTRY)

    def readCard(self):
        return str(+int('0x' + self.device.scan_field().hex(), 0))

    def cleanup(self):
        # Not sure if something needs to be done here.
        logger.debug("PN532Reader clean up.")


class Reader(object):
    def __init__(self):
        self.devs = list()
        path = os.path.dirname(os.path.realpath(__file__))
        if not os.path.isfile(path + '/deviceName.txt'):
            sys.exit('Please run RegisterDevice.py first')
        else:
            with open(path + '/deviceName.txt', 'r') as f:
                device_keys = f.readlines()
            devices = get_devices()
            for device in devices:
                for dev_key in device_keys:
                    dev_name_phys = dev_key.rstrip().split(';', 1)
                    dev_name = dev_name_phys[0]
                    dev_phys = ''
                    if len(dev_name_phys) > 1:
                        dev_phys = dev_name_phys[1]
                    if device.name == dev_name and device.phys == dev_phys:
                        if dev_name == 'MFRC522':
                            self.devs.append(Mfrc522Reader())
                        elif dev_name == 'RDM6300':
                            self.devs.append(Rdm6300Reader())
                        elif dev_name == 'PN532':
                            self.devs.append(Pn532Reader())
                        else:
                            try:
                                usb_reader = UsbReader(device)
                                self.devs.append(usb_reader)
                            except IndexError:
                                sys.exit('Could not find the device %s.\n Make sure it is connected' % dev_name)
                        break

    def readCard(self):
        que = SimpleQueue()
        threads_list = list()

        for dev in self.devs:
            t = multiprocessing.Process(target=lambda q: q.put(dev.readCard()), args=(que,))
            t.start()
            threads_list.append(t)

        found_result = False
        while not found_result:
            for process in threads_list:
                process.join(0.001)
                if not process.is_alive():
                    found_result = True
                    break

        for process in threads_list:
            process.terminate()

        return que.get()
